package edu.xhu.video.service.impl;

import cn.hutool.json.JSONUtil;
import com.xxl.job.core.handler.annotation.XxlJob;
import edu.xhu.api.user.UserClient;
import edu.xhu.common.exception.CustomException;
import edu.xhu.model.biliuser.dtos.RatioDto;
import edu.xhu.model.biliuser.enums.AuthType;
import edu.xhu.model.biliuser.enums.LevelEnum;
import edu.xhu.model.biliuser.enums.MegaType;
import edu.xhu.model.common.constants.CheckConstants;
import edu.xhu.model.common.dtos.ResponseResult;
import edu.xhu.model.common.enums.AppHttpCodeEnum;
import edu.xhu.model.video.dtos.BiliVideoCheckDto;
import edu.xhu.model.video.enums.VideoStatus;
import edu.xhu.model.video.pojos.BiliVideo;
import edu.xhu.utils.common.SensitiveWordUtil;
import edu.xhu.video.aliyun.Scan;
import edu.xhu.video.mapper.BiliVideoMapper;
import edu.xhu.video.service.VideoCheckService;
import lombok.RequiredArgsConstructor;
import lombok.Value;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;

import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.locks.ReentrantLock;
import java.util.stream.Collectors;

/**
 * 视频审核服务类
 */
@Service
@RequiredArgsConstructor
@Slf4j
public class VideCheckServiceImpl implements VideoCheckService {

    private final UserClient userClient;

    private final BiliVideoMapper biliVideoMapper;

    private final StringRedisTemplate redisTemplate;

    private final Executor checkExecutor;

    private final ReentrantLock lock = new ReentrantLock();

    private final Comparator<BiliVideo> comparator = new Comparator<BiliVideo>() {
        @Override
        public int compare(BiliVideo o1, BiliVideo o2) {
            ResponseResult<?> ratioResO1 = userClient.getRatioDataByUserId(o1.getUserId());
            ResponseResult<?> ratioRes02 = userClient.getRatioDataByUserId(o2.getUserId());
            if (!ratioResO1.getCode().equals(200) || !ratioRes02.getCode().equals(200)) {
                throw new CustomException(AppHttpCodeEnum.DATA_NOT_EXIST);
            }
            RatioDto ratio1 = JSONUtil.toBean(ratioResO1.getData().toString(), RatioDto.class);
            RatioDto ratio2 = JSONUtil.toBean(ratioRes02.getData().toString(), RatioDto.class);
            //配置认证,奖励1000分
            double ratioAuth1 = doRatioAuth(ratio1.getIdentityAuthentication());
            double ratioAuth2 = doRatioAuth(ratio2.getIdentityAuthentication());
            //配置等级
            double ratioLevel1 = doRatioLevel(ratio1.getLevel());
            double ratioLevel2 = doRatioLevel(ratio2.getLevel());
            //配置大会员,奖励10000分
            double ratioMega1 = doRatioMega(ratio1.getMega());
            double ratioMega2 = doRatioMega(ratio2.getMega());
            //配置其他
            double ratioFans = 3.0;
            double ratioLikes = 2.5;
            double ratioViews = 5.0;
            return Double.compare(
                    ratioAuth1 * 1000
                            + ratioLevel1 * Long.parseLong(ratio1.getExperience())
                            + ratioMega1 * 10000
                            + ratioFans * ratio1.getFans()
                            + ratioLikes * ratio1.getLikes()
                            + ratioViews * ratio1.getPlay()
                    , ratioAuth2 * 1000
                            + ratioLevel2 * Long.parseLong(ratio2.getExperience())
                            + ratioMega2 * 10000
                            + ratioFans * ratio2.getFans()
                            + ratioLikes * ratio2.getLikes()
                            + ratioViews * ratio2.getPlay());
        }
    };

    //审核阻塞队列,为什么用checkDto,答:节省内存
    private final LinkedBlockingQueue<BiliVideoCheckDto> linkedBlockingQueue
            = new LinkedBlockingQueue<>();

    /**
     * 视频审核提交
     */
    @Override
    public void videoCheckCommit(BiliVideo biliVideo) {
        BiliVideoCheckDto checkingVideo = new BiliVideoCheckDto();
        BeanUtils.copyProperties(biliVideo, checkingVideo);
        try {
            //这里提前调用视频审核的方法,审核视频,放入阻塞队列
            Map<String, Object> videoScan = Scan.greenVideoScan(checkingVideo.getVideoUrl());
            if (!videoScan.get("code").equals(200) || Objects.isNull(videoScan.get("taskId"))) {
                biliVideo.setState(VideoStatus.FAIL.getCode());
            }
            //视频在审核了,加入到审核队列中,设置状态checking
            else {
                Map<String, String> taskMap = new HashMap<>();
                taskMap.put("videoId", checkingVideo.getVideoId().toString());
                taskMap.put("taskId", videoScan.get("taskId").toString());
                checkingVideo.setTaskMap(taskMap);
                this.linkedBlockingQueue.put(checkingVideo);   //加入到阻塞队列当中
                lock.unlock();
                biliVideo.setState(VideoStatus.CHECKING.getCode());
            }
        } catch (Exception e) {
            log.error("添加阻塞队列失败:{}", e.toString());
            biliVideo.setState(VideoStatus.FAIL.getCode());
            throw new CustomException(AppHttpCodeEnum.SERVER_ERROR);
        }
        //更新video状态
        biliVideoMapper.updateById(biliVideo);
    }

    /**
     * 定时任务审核(线程安全,不用担心map错位)
     */
    @XxlJob(value = "checkHandler")
    public void CheckBlockingQueue() {
        BiliVideoCheckDto checkVideo = this.linkedBlockingQueue.poll();
        //不为空,执行审核
        if (checkVideo != null) {
            checkVideo.setState(VideoStatus.CHECKING.getCode());
            //先从缓存拿
            Object o = redisTemplate.opsForHash().get(CheckConstants.CHECK_HASH, checkVideo.getVideoId().toString());
            if (o != null) {
                String videoId = o.toString();
                if (!videoId.equals(checkVideo.getVideoId().toString())) {
                    //缓存里有脏东西
                    redisTemplate.opsForHash().delete(CheckConstants.CHECK_HASH, checkVideo.getVideoId());
                }
                //开始审核吧
                CompletableFuture.runAsync(() -> doCheck(checkVideo, true), checkExecutor);
            } else {
                //缓存没有,去db找
                BiliVideo dbVideo = biliVideoMapper.selectById(checkVideo.getVideoId());
                if (dbVideo != null) {
                    //db有,缓存没有,什么情况?(被别人消费了?redis挂了?),查一下审核状态
                    //如果不是审核中状态,奇了怪了,不是审核中状态却在阻塞队列里,脏东西,不做处理
                    if (dbVideo.getState().equals(VideoStatus.CHECKING.getCode())) {
                        //如果是审核中状态,却不在队列里,怪了怪了,审核吧
                        CompletableFuture.runAsync(() -> doCheck(checkVideo, false), checkExecutor);
                    }
                }
                //db也没有,幽灵数据,不做任何处理
            }
        }
    }

    /**
     * 审核了
     */
    private void doCheck(BiliVideoCheckDto checkVideo, boolean cacheFlag) {
        //0.先审核视频,视频还没好,都先别动,不删缓存,给我重新入队
        //0.其他情况,删缓存,更新video,出队
        Map<String, String> videoTask = checkVideo.getTaskMap();
        String taskId = Objects.requireNonNull(videoTask).get("taskId");
        Map<String, Object> videoResult = Scan.getVideoResult(taskId);
        //如果不为200或者280,表明视频审核报错
        if (!videoResult.get("code").equals(200) || !videoResult.get("code").equals(280)) {
            //报错,删缓存,改状态,return
            redisTemplate.opsForHash().delete(CheckConstants.CHECK_HASH, checkVideo.getVideoId().toString());
            biliVideoMapper.updateVideoStatusById(checkVideo.getVideoId(), VideoStatus.FAIL.getCode());
            return;
        }
        //还没审核好
        if (videoResult.get("frameResult") == null || videoResult.get("frameNum") == null) {
            //还没审核好,审核重新入队
            try {
                BiliVideoCheckDto reDto = new BiliVideoCheckDto();
                BeanUtils.copyProperties(checkVideo, reDto);
                this.linkedBlockingQueue.put(reDto);
            } catch (Exception e) {
                log.error("审核重新入队出错:{}", e.toString());
                redisTemplate.opsForHash().delete(CheckConstants.CHECK_HASH, checkVideo.getVideoId().toString());
                biliVideoMapper.updateVideoStatusById(checkVideo.getVideoId(), VideoStatus.FAIL.getCode());
                lock.unlock();
                return;
            }
            lock.unlock();
        }
        //1.不管怎样,redis都得删掉
        if (cacheFlag) {
            redisTemplate.opsForHash().delete(CheckConstants.CHECK_HASH, checkVideo.getVideoId().toString());
        }
        //审核好了
        Object frameNum = videoResult.get("frameNum");
        int num = Integer.parseInt(frameNum.toString());
        //审核失败
        if (num > 0) {
            biliVideoMapper.updateVideoStatusById(checkVideo.getVideoId(), VideoStatus.FAIL.getCode());
            return;
        }
        //审核流程->1.视频内容 2.标题,简介,标签 3.视频封面
        //2.先过滤敏感词
        String[] split = checkVideo.getTags().split(",");
        List<String> tags = Arrays.stream(split).collect(Collectors.toList());
        List<String> SensitiveWordList = new ArrayList<>();
        SensitiveWordList.add("司马");
        SensitiveWordList.add("法轮功");
        SensitiveWordList.add("冰毒");
        SensitiveWordUtil.initMap(SensitiveWordList);
        List<String> isSensitive = tags.stream().parallel().filter(str -> !SensitiveWordUtil.matchWords(str).isEmpty()).collect(Collectors.toList());
        //有敏感词,审核失败!
        if (isSensitive.size() > 0) {
            biliVideoMapper.updateVideoStatusById(checkVideo.getVideoId(), VideoStatus.FAIL.getCode());
            return;
        }
        //3.标题,简介,标签,打包送入oss
        String content = checkVideo.getTitle()
                .concat(".")
                .concat(checkVideo.getDescription())
                .concat(".")
                .concat(checkVideo.getTags());
        Map<String, Object> textScan = Scan.greenTextScan(content);
        if (!textScan.get("code").equals(200) || textScan.get("labels") != null || textScan.get("reason") != null) {
            biliVideoMapper.updateVideoStatusById(checkVideo.getVideoId(), VideoStatus.FAIL.getCode());
            return;
        }
        //4.视频封面,送入图像审核
        Map<String, Object> imageScan = Scan.greenImageScan(checkVideo.getCoverUrl());
        if (!imageScan.get("code").equals(200) || !imageScan.get("labels").equals("nonLabel") || imageScan.get("confidence") != null) {
            biliVideoMapper.updateVideoStatusById(checkVideo.getVideoId(), VideoStatus.FAIL.getCode());
            return;
        }
        //5.如果全都没问题,审核成功！两个queue都poll了,缓存也删了,更新video状态
        try {
            biliVideoMapper.updateVideoStatusById(checkVideo.getVideoId(), VideoStatus.PASS.getCode());
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * 一些权重比方法
     */
    private double doRatioAuth(String authType) {
        double ratioAuth;
        if (authType.equals(AuthType.NORMAL.getCode())) {
            ratioAuth = 0.2;
        } else if (authType.equals(AuthType.Person.getCode())) {
            ratioAuth = 0.3;
        } else {
            ratioAuth = 0.5;
        }
        return ratioAuth;
    }

    private double doRatioLevel(String level) {
        double ratioLevel;
        if (level.equals(LevelEnum.LEVEL_ZERO.getLevel())) {
            ratioLevel = 0.05;
        } else if (level.equals(LevelEnum.LEVEL_ONE.getLevel())) {
            ratioLevel = 0.1;
        } else if (level.equals(LevelEnum.LEVEL_TWO.getLevel())) {
            ratioLevel = 0.2;
        } else if (level.equals(LevelEnum.LEVEL_THREE.getLevel())) {
            ratioLevel = 0.3;
        } else if (level.equals(LevelEnum.LEVEL_FORE.getLevel())) {
            ratioLevel = 0.4;
        } else if (level.equals(LevelEnum.LEVEL_FIVE.getLevel())) {
            ratioLevel = 0.5;
        } else if (level.equals(LevelEnum.LEVEL_SIX.getLevel())) {
            ratioLevel = 0.6;
        } else {
            ratioLevel = 1.0;
        }
        return ratioLevel;
    }

    private double doRatioMega(String mega) {
        double ratioMega;
        if (mega.equals(MegaType.COMMON.getCode())) {
            ratioMega = 0.5;
        } else if (mega.equals(MegaType.MONTH.getCode())) {
            ratioMega = 1.0;
        } else if (mega.equals(MegaType.QUARTER.getCode())) {
            ratioMega = 1.5;
        } else {
            ratioMega = 2.0;
        }
        return ratioMega;
    }


}
